<?php
namespace Libs;
use Exception;

/**
* 一些常用的函数
*/
class Fun
{
    public static $instance;
    public static $Error = '';
    
    public static function getInstance()
    {
        if (!Fun::$instance) {
            self::$instance = new Fun();
        }
        return self::$instance;
    }
    
    private function __construct()
    {
    }
    
    /**
     * 调用淘宝接口, 可以获取国内外的ip归属地
     * @param $ip
     * @return array|mixed
     */
    public static function getAddress($ip)
    {
        if (empty($ip)) {
            return array();
        }
        $url = "http://ip.taobao.com/service/getIpInfo.php?ip={$ip}";
        
        $output = file_get_contents($url);
        //{"code":0,"data":{"ip":"183.192.33.78","country":"中国","area":"","region":"上海","city":"上海","county":"XX","isp":"移动","country_id":"CN","area_id":"","region_id":"310000","city_id":"310100","county_id":"xx","isp_id":"100025"}}
        return json_decode($output, TRUE);
        
    }
    
    /**
     * 只获取省市信息
     * @param $ip
     * @return array
     */
    public static function getProvinceCityByIp($ip)
    {
        $info = self::getAddress($ip);
        if ($info['code'] == 0) {
            $province = $info['data']['region'];
            $city     = $info['data']['city'];
        } else {
            $province = '';
            $city     = '';
        }
        
        return array('province' => $province, 'city' => $city);
        
    }
    
    //毫秒数64位
    public static function getMsecTime()
    {
        list($usec, $sec) = explode(' ', microtime());
        
        $usec2msec     = $usec * 1000;  //计算微秒部分的毫秒数(微秒部分并不是微秒,这部分的单位是秒)
        $usec2msec2int = intval($usec2msec);
        $sec2msec      = $sec * 1000;    //计算秒部分的毫秒数
        $sec2msec2int  = intval($sec2msec);
        
        $msec = $sec2msec2int + $usec2msec2int; //加起来就对了
        return $msec;
    }
    
    //位图排序
    public static function BitMapSort($a)
    {
        // $a = array(1,4,3,50,34,60,100,88,200,150,300); //定义一个乱序的数组
        // var_dump(PHP_INT_MAX, PHP_INT_SIZE);
        // int 9223372036854775807 对应mysql的bigint
        // int 8
        
        //申请一个整形数组, 初始化为整数0
        $size   = count($a);
        $bitmap = array_fill(0, $size, 0);
        
        // $bitmap中每个整形的二进制位数 
        // 本例中int = 8*8 = 64bit; $bitmap数组一共1000*64 = 64000个bit位
        // 也就是说能为最大值等于64000的整数集合排序
        $int_bit_size = PHP_INT_SIZE * 8;
        // $a = array(1,4,3,50,34,60,100,88,200,150,300); //定义一个乱序的数组
        
        //扫描$a中的每一个数, 将其转换为 x*64 + y
        foreach ($a as $v) {
            $shang = $v / $int_bit_size; //商
            $yushu = $v % $int_bit_size; //余数
            
            $offset = 1 << $yushu;
            
            $bitmap[$shang] = $bitmap[$shang] | $offset;//将bit位置为1
        }
        
        //将$bitmap中的bit位依次还原为整数输出,即可得到排序后的数组
        $b = array();
        foreach ($bitmap as $k => $v) {
            for ($i = 0; $i < $int_bit_size; $i++) {
                $tmp  = 1 << $i;
                $flag = $tmp & $bitmap[$k];
                
                // $b[] = $flag ? $k * $int_bit_size + $i : false;
                if ($flag) {
                    $b[] = $k * $int_bit_size + $i;
                }
            }
        }
        
        return $b;
    }
    
    //按照二维数组中的某个键进行排序
    public static function sort2DArray(&$arr, $key, $desc = '')
    {
        if (empty($arr)) {
            return [];
        }

        $tmp = array();
        foreach ($arr as $k => $v) {
            $tmp[$k] = $v[$key];
        }
        
        if ($desc) {
            arsort($tmp);
        } else {
            asort($tmp);
        }
        
        $result = array();
        foreach ($tmp as $k => $v) {
            $result[$k] = $arr[$k];
        }
        
        return $result;
    }
    
    //数字索引数组排序
    public static function sort2DArrayIndex(&$arr, $key, $desc = '')
    {
        if (empty($arr)) {
            return [];
        }

        $tmp = array();
        foreach ($arr as $k => $v) {
            $tmp[$v[$key]] = $v;
        }

        if ($desc) {
            krsort($tmp);
        } else {
            ksort($tmp);
        }

        $arr = array_values($tmp);
        return $arr;
    }

    /**
     * @param int $len
     * @return string
     * desc 生成随机码
     * 注意里边有+, >, < 等特殊字符在不同编码的时候会又变化
     */
    public static function randCode($len = 10)
    {
        $char    = array(
            'Q', '@', '8', 'y', '%', '^', '5', 'Z', '(', 'G', '_', 'O', '`', 'S', '-',
            'N', '<', 'D', '{', '}', '[', ']', 'h', ';',
            'W', '.', '/', '|', ':', '1', 'E', 'L', '4', '&', '6', '7', '#', '9',
            'a', 'A', 'b', 'B', '~', 'C', 'd', '>', 'e', '2', 'f', 'P',
            'g', ')', '?', 'H', 'i', 'X', 'U', 'J', 'k', 'r', 'l', '3', 't', 'M',
            'n', '=', 'o', '+', 'p', 'F', 'q', '!', 'K', 'R', 's',
            'c', 'm', 'T', 'v', 'j', 'u', 'V', 'w', ',', 'x', 'I', '$', 'Y', 'z', '*'
        );
        $charLen = count($char) - 1;
        $token   = '';
        for ($i = 0; $i < $len; $i++) {
            $index = mt_rand(0, $charLen);
            $token .= $char[$index];
        }
        
        return $token;
    }
    
    public static function randChar($len = 10)
    {
        $char = array(
            'Q', '8', 'y', '5', 'Z', 'G', 'O', 'S', 'N', 'D', 'h',
            'W', '1', 'E', 'L', '4', '6', '7', '9',
            'a', 'A', 'b', 'B', 'C', 'd', 'e', '2', 'f', 'P',
            'g', 'H', 'i', 'X', 'U', 'J', 'k', 'r', 'l', '3', 't', 'M',
            'n', 'o', 'p', 'F', 'q', 'K', 'R', 's', 'z',
            'c', 'm', 'T', 'v', 'j', 'u', 'V', 'w', 'x', 'I', 'Y',
        );
        $charLen = count($char) - 1;
        $token   = '';
        for ($i = 0; $i < $len; $i++) {
            $index = mt_rand(0, $charLen);
            $token .= $char[$index];
        }
        
        return $token;
    }
    
    /**
     * desc 获取本模块下所有的控制器, 及其方法和注释信息
     * 注意:
     * 1. PHP代码缓存类的扩展要保留代码的注释信息, 否则这里获取不到注释信息
     * 2. 代码注释要用多行注释
     * 3. 注释中希望获取的文字单独占一行, 并且以字符串 "desc "开始
     * 4. todo 公共访问模块,无限制
     * @param string $moduleName 模块名
     * @return array
     * @throws Exception
     */
    public static function getAllController($moduleName = '')
    {
        $moduleName  = empty($moduleName) ? MODULE_NAME : $moduleName;
        $dir         = MODULEPATH . $moduleName . '/';
        $controllers = scandir($dir);
        
        $blackMethod = ['__construct', '__distruct'];
        $blackClass  = [''];
        
        $arr = [];
        foreach ($controllers as $key => $ctrl) {
            $path = $dir . $ctrl;
            if (is_file($path)) {
                require_once($path);
                
                $ctrlname  = $moduleName . '_' . str_replace(PHP_FILE_EXTENSION, '', $ctrl); //用下划线拼接, admin_index
                $classname = '_' . str_replace(PHP_FILE_EXTENSION, '', $ctrl);
                if (class_exists($classname)) {
                    $class = new \ReflectionClass($classname);
                } else {
                    continue;
                }
                
                $Ctrlcomment = $class->getDocComment();
                preg_match('/desc\s+(.*)/', $Ctrlcomment, $match);
                $arr[$ctrlname]['class']['doc']  = !empty($match[1]) ? trim($match[1]) : '';
                $arr[$ctrlname]['class']['name'] = $ctrlname;
                
                $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
                foreach ($methods as $method) {
                    //方法名和类名不在黑名单, 而且不是父类的方法
                    if (in_array($method->name, $blackMethod) === FALSE && in_array($method->class, $blackClass) === FALSE && strpos($method->class, $classname) !== FALSE) {
                        $Methodcomment = $method->getDocComment();
                        preg_match('/desc\s+(.*)/', $Methodcomment, $match);
                        $tmp                        = [
                            'doc'  => !empty($match[1]) ? trim($match[1]) : '',
                            'name' => $method->name,
                        ];
                        $arr[$ctrlname]['method'][] = $tmp;
                        
                        //找不到注释就不获取
//						if (!empty($match[1])) {
//							$tmp = [
//								'doc' => trim($match[1]),
//								'name' => $method->name,
//							];
//							$arr[$ctrlname]['method'][] = $tmp;
//						}
                    }
                }
            }
        }
        
        return $arr;
    }
    
    /**
     * @param string $data 明文
     * @param int $len 长度
     * @return string
     * 不可逆加密, 用于登录密码加密
     */
    public static function saltmd5($data, $len = 32)
    {
        $salt = '@#$*&%![}=!';
        $all  = md5(md5($data) . $salt);
        return substr($all, 0, $len);
    }
    
    /**
     * 正则匹配出汉字
     * @param $string
     * @return mixed
     */
    public static function getCn($string)
    {
        preg_match("/[\x{4e00}-\x{9fa5}]+/u", $string, $match);
        return $match;
    }
    
    //获取倒计时
    public static function getRemainTimeDesc($timestamp)
    {
        return [
            'day'    => $timestamp / 86400,
            'hour'   => ($timestamp % 86400) / 3600,
            'minute' => (($timestamp % 86400) % 3600) / 60,
            'sec'    => (($timestamp % 86400) % 3600) % 60,
        ];
    }
    
    //判断数字所在的区间
    public static function numberPosition($current, $start, $end)
    {
        if ($current < $start) {
            return 0;
        } elseif ($current >= $start && $current <= $end) {
            return 1;
        } else {
            return 2;
        }
    }
    
    /**
     * 格式化显示时间戳
     * @param $timestamp
     * @return false|string
     */
    public static function formatDate($timestamp)
    {
        return date('Y-m-d H:i:s', $timestamp);
    }
    
    public static function getDateTime($timestamp='')
    {
        if (!empty($timestamp)) {
            return date('Y-m-d H:i:s', $timestamp);
        } else {
            return date('Y-m-d H:i:s');
        }
    }
    
    /**
     * 构建URL
     * @param string $path
     * @param array $arg
     * @return string
     */
    public static function buildUrl($path, $arg = array())
    {
        if (strpos($path, 'http') !== FALSE) {
            $url = $path;
        } else {
            $url = BASEURL . $path;
        }
        
        if (empty($arg)) {
            return $url;
        } else {
            if (strpos($url, '?') !== FALSE) {
                $url .= '&' . http_build_query($arg);
            } else {
                $url .= '?' . http_build_query($arg);
            }
            return $url;
        }
    }
    
    /**
     * desc 点分小版本最多4位
     * @param string $va 待检测版本
     * @param string $vb 基准版本
     * @return int 如果 va 小于 vb 返回 < 0； 如果 va 大于 vb 返回 > 0；如果两者相等，返回 0
     */
    public static function compareVersion($va, $vb)
    {
        //小版本号补前导零
        $arrVersionA    = explode('.', $va);
        $formatVersionA = '';
        foreach ($arrVersionA as $v) {
            $formatVersionA .= str_pad($v, 4, '0', STR_PAD_LEFT);
        }
        
        //小版本号补前导零
        $arrVersionB    = explode('.', $vb);
        $formatVersionB = '';
        foreach ($arrVersionB as $v) {
            $formatVersionB .= str_pad($v, 4, '0', STR_PAD_LEFT);
        }
        
        //去掉末尾的0
        $formatVersionA = rtrim($formatVersionA, '0');
        $formatVersionB = rtrim($formatVersionB, '0');
        
        // echo $formatVersionA, ' ', $formatVersionB, ' ';
        
        return strcmp($formatVersionA, $formatVersionB);
    }
    
    /**
     * 将两个数据集进行关联, 需要指定用来关联的字段名
     * @param array $masterData 主数据集 (需要是二维数组)
     * @param array $branchData 附属数据集 (需要是二维数组)
     * @param string $masterFieldName 主数据中用来关联的字段名
     * @param string $branchFieldName 附属数据中用来关联的字段名
     * @param string $fieldName 关联后, 附属数据在主数据中新的字段名
     * @param string $fieldValue 关联后, 附属数据在主数据中新的字段名的值
     * @throws Exception
     */
    public static function linkData(&$masterData, &$branchData, $masterFieldName, $branchFieldName, $fieldName='', $fieldValue='')
    {
        $isMaster2D = is_array(reset($masterData)) ? TRUE : FALSE; //主数据是否是二维数组
    
        $fieldName = empty($fieldName) ? $masterFieldName : $fieldName;
        
        if (!$isMaster2D) {
            throw new Exception(__METHOD__ . ' 主数据不是二维数组'); //主数据不是二维数组, 没必要调用这个方法
        }
        
        //将附属数据变为以关联字段的值为键的二维数组
        $arrBranch = array();
        foreach ($branchData as $v) {
            if (!empty($fieldValue) && !empty($v[$fieldValue])){
                $arrBranch[$v[$branchFieldName]] = $v[$fieldValue];
            } else {
                $arrBranch[$v[$branchFieldName]] = $v;
            }
        }
        
        foreach ($masterData as $key => $value) {
            $relData = $value[$masterFieldName];
            if (!empty($arrBranch[$relData])) {
                $masterData[$key][$fieldName] = $arrBranch[$relData];
            } else {
                $masterData[$key][$fieldName] = array();
            }
        }
    }

    /**
     * 用一维数组的某个字段值分组，生成二维数组
     * @param $arr
     * @param $field
     * @return array
     */
    public static function groupArrayByField(&$arr, $field)
    {
        $list = [];
        foreach ($arr as $item) {
            $v = $item[$field] ?? '';
            $list[$v][] = $item;
        }
        return $list;
    }
    
    /**
     * 用指定分隔符, 组装键值对字符串, 不对数据编码
     * @param $arr
     * @param string $glue1
     * @param string $glue2
     * @return string
     */
    public static function buildStr($arr, $glue1 = '=', $glue2 = '&')
    {
        $list = [];
        foreach ($arr as $k => $v) {
            $list[] = "{$k}{$glue1}{$v}";
        }
        
        return implode($glue2, $list);
        
    }
    
    /**
     * 遍历某个路径下的所有文件和目录, 去掉 . 和 ..
     * @param $dir
     * @param $black
     * @return array
     */
    public static function scanDir($dir, $black=array())
    {
        $arr = scandir($dir);
        $black[] = '.';
        $black[] = '..';
        return array_values(array_diff($arr, $black));
    }
    
    /**
     * 将字节转换为其他单位
     * @param $byte
     * @param $unit
     * @return string|null
     */
    public static function formatByte($byte, $unit)
    {
        $unit = strtolower($unit);
        
        switch ($unit) {
            case 'k': //KB
                $rs = bcdiv($byte, '1024', 2);
                break;
            case 'm': //MB
                $k = bcdiv($byte, '1024', 2);
                $rs = bcdiv($k, '1024', 2);
                break;
            case 'g': //GB
                $k = bcdiv($byte, '1024', 2);
                $m = bcdiv($k, '1024', 2);
                $rs = bcdiv($m, '1024', 2);
                break;
            case 't': //TB
                $k = bcdiv($byte, '1024', 2);
                $m = bcdiv($k, '1024', 2);
                $g = bcdiv($m, '1024', 2);
                $rs = bcdiv($g, '1024', 2);
                break;
            default:
                $rs = $byte;
                break;
        }
        
        return $rs;
    }
    
    /**
     * 分割字符串, 支持多字节
     * @param $str
     * @param $width
     * @return array
     */
    public static function strSlice($str, $width)
    {
        $chars = mb_strlen($str); //总字符数
        $n = ceil($chars / $width); //总行数
        
        $lines = [];//每行的字符
        for ($i=0; $i<$n; $i++) {
            $lines[] = mb_substr($str, $i*$width, $width);
        }
        
        return $lines;
    }
    
    //输入上下级的一维数组, 输出链条; 注意, 根元素要出现在数组前边
    public static function getChains($dic)
    {
        $list = [];
        foreach($dic as $id => $parent_id){
            $list[$parent_id][$id] = 1;
        }
        
        $stacks = []; //栈, 最后一级经销商 => 以逗号隔开的上级经销商
        
        foreach ($dic as $id => $parent_id) {
            unset($list[$parent_id][$id]); //删除次级经销商, 为了方便判断某个上级经销商的次级经销商是否都处理过
            
            if (empty($stacks[$parent_id])) {
                $stacks[$id] = $parent_id.','.$id;
                
            } else {
                $stacks[$id] = $stacks[$parent_id].','.$id; //整合成新的
                
                if (empty($list[$parent_id])) {
                    unset($stacks[$parent_id]); // 删除中间链条, 比如, b只有两个次级经销商c,d, 和一个上级经销商a, 那么找出a,b,c 和 a,b,d 两个链条后, 就把中间链条a,b删除掉, 只保留最长的链条
                }
            }
        }
        
        return $stacks;
    }
    
    //笛卡尔拼接, $list 需要是二维数组, 至少两个子数组
    public static function cartesian($list, $char=',')
    {
        $total = count($list);
        
        if ($total == 0) {
            return $list;
        }
        
        if ($total == 1) {
            return reset($list);
        }

        $total -= 1;

        $i = $total-1;
        $j = $total;

        while ($i >= 0) {
            $tmp = array();
            foreach ($list[$i] as $vi) {
                foreach ($list[$j] as $vj) {
                    $tmp[] = $vi.$char.$vj;
                }
            }

            $list[$i] = $tmp;

            $i--;
            $j--;
        }
        
        return $list[0];
        
        /*
         $list = [
            ['红色', '黄色'],
            [39,40,41],
            ['高帮','低帮'],
            ['猪皮', '牛皮']
        ];
    
        $rs = Fun::cartesian($list);
         */
    }
    
    /**
     * 加密解密
     * @param String $data 明文/密文
     * @param bool|true $encode true:加密; false:解密
     * @return mixed|string 密文/明文
     */
    public static function encrypt($data, $encode = true)
    {
        $config = Config::$encryption;
        $privateKey = $config['key'];
        if ($encode) {//加密
            $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $privateKey, $data, MCRYPT_MODE_ECB);
            return base64_encode($encrypted);
        } else { //解密
            $encryptedData = base64_decode($data);
            $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $privateKey, $encryptedData, MCRYPT_MODE_ECB);
            return trim($decrypted);
        }
    }
    
    /**
     * 加密解密
     * @param string $data 明文/密文
     * @param bool|true $encode true:加密; false:解密
     * @return mixed|string 密文/明文
     */
    public static function openssl($data, $encode = true)
    {
        $openssl_method = 'AES-128-CBC';
        $openssl_password = 'F0C1BC1B1F38F83F'; //16位
        $openssl_iv = 'B5B206C908C016D9'; //16位
        
        if ($encode) {
            return openssl_encrypt($data, $openssl_method, $openssl_password, 0, $openssl_iv);
        } else {
            return openssl_decrypt($data, $openssl_method, $openssl_password, 0, $openssl_iv);
        }
        
    }

    /**
     * 传入点分字符串, 获取多维数组的值
     * @param array $arr
     * @param string $key 点分字符串
     * @return array|string
     */
    public static function getSubArray($arr, $key)
    {
        $arrKey = explode('.', $key);
        $count = count($arrKey);
        $data = $arr[$arrKey[0]];
        for ($i=1; $i<$count; $i++) {
            $k = $arrKey[$i];
            $data = !empty($data[$k]) ? $data[$k] : [];
        }
        return $data;
    }

    /**
     * @param string $keyword 进程中包含的字符串
     * @param int $num 数量是多少个才算是已经运行
     * @return bool
     */
    public static function isProcessRun($keyword, $num=2)
    {
        $cmdCheck = "ps -ef | grep $keyword | grep -v grep | wc -l";
        exec($cmdCheck, $output);
        if ($output[0] > $num) {
            return true;
        } else {
            return false;
        }
    }

    public static function log($msg)
    {
        FileLog::info($msg);
    }

    //执行系统命令
    public static function exec($cmd)
    {
        $output = [];
        $return = '';

        exec($cmd, $output, $return);

        return ['output' => $output, 'return' => $return];
    }

    /**
     * 传入数组, 筛选想要的字段
     * @param array $arr 原始数据
     * @param array $filter 需要的数据 [字段名 => 默认值]
     * @return array
     */
    public static function pickArray($arr, $filter)
    {
        return array_merge($filter, array_intersect_key($arr, $filter));
    }

    //数组 去空 + 去重
    public static function filterArray($arr)
    {
        return array_unique(array_filter($arr));
    }

    //字符串 去空 + 去重
    public static function filterString($str, $gap=',')
    {
        $arr = array_unique(array_filter(explode($gap, $str)));
        return implode($gap, $arr);
    }

    //替换字符串中的字符
    public static function replaceChar($str, $rule=[])
    {
        if (empty($rule)) {
            return $str;
        }
        return str_replace(array_keys($rule), $rule, $str);
    }

    //将数组中的索引项目替换成关联项
    public static function arrayIdx2Assoc($arr)
    {
        $rs = [];
        foreach ($arr as $k => $v) {
            if (is_int($k)) {
                $rs[$v] = $v;
            } else {
                $rs[$k] = $v;
            }
        }
        return $rs;
    }

    /**
     * 将一维数组转为字符串, 并且是PHP的标准数组格式
     * @param array $arr 源数组
     * @param array $filter 关联数组, 将$arr中的键改个名字, 并且只返回$filter中指定的键
     */
    public static function array2str($arr, $filter=[], $eol='')
    {
        $keyMap = self::arrayIdx2Assoc($filter);

        $a = [];
        foreach ($arr as $k => $v) {
            if (!empty($filter) && !isset($filter[$k])) {
                continue;
            }

            $key = isset($keyMap[$k]) ? $keyMap[$k] : $k;

            if (is_numeric($v)) {
                $value = $v;
            } else {
                $value = "'$v'";
            }

            $a[] = "'$key' => $value";
        }
        $s = "[". implode(','.$eol, $a) ."]";
        return $s;
    }

}
